package cn.tqfeiyang.smartadmin.commons.security;

import cn.dev33.satoken.annotation.SaIgnore;
import cn.dev33.satoken.exception.SaTokenException;
import cn.dev33.satoken.stp.StpUtil;
import cn.dev33.satoken.strategy.SaStrategy;
import cn.tqfeiyang.smartadmin.commons.response.ResultCode;
import cn.tqfeiyang.smartadmin.commons.toolbox.EnvironmentUtils;
import cn.tqfeiyang.smartadmin.commons.toolbox.ResponseUtils;
import cn.tqfeiyang.smartadmin.commons.toolbox.StringUtils;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;

/**
 * 安全拦截器
 *
 * @author tqfeiyang
 * @since 2024/8/22 8:49
 */
@Slf4j
@Component
public class SecurityInterceptor implements HandlerInterceptor {

    @Resource
    private SecurityService securityService;

    /**
     * token 最低活跃频率（单位：秒），如果 token 超过此时间没有访问系统就会被冻结，默认-1 代表不限制，永不冻结
     * 例如可以设置为 1800 代表 30 分钟内无操作就冻结
     */
    @Value("${sa-token.active-timeout}")
    private long tokenActiveTimeout;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // OPTIONS请求直接return
        if (HttpMethod.OPTIONS.toString().equals(request.getMethod())) {
//            // 允许指定域访问跨域资源
//            response.setHeader("Access-Control-Allow-Origin", "*");
//            // 允许所有请求方式
//            response.setHeader("Access-Control-Allow-Methods", "*");
//            // 允许的header参数
//            response.setHeader("Access-Control-Allow-Headers", "*");
//            // 有效时间
//            response.setHeader("Access-Control-Max-Age", "3600");
//            // 返回状态
//            response.setStatus(HttpStatus.NO_CONTENT.value());
            return false;
        }

        if (!(handler instanceof HandlerMethod)) {
            return true;
        }

        HandlerMethod handlerMethod = (HandlerMethod) handler;

        try {
            // --------------- 第一步： 根据token 获取用户 ---------------
            String tokenValue = StpUtil.getTokenValue();
            boolean isDebugNumberToken = EnvironmentUtils.isDevelopment() && StringUtils.isNumeric(tokenValue);

            String loginId = null;

            if (isDebugNumberToken) {
                //开发测试环境，且token为数字，则表明为 调试临时用户，即需要调用 sa-token switch
                loginId = securityService.formatLoginIdByNumberToken(Long.parseLong(tokenValue));
                StpUtil.switchTo(loginId);
            } else {
                //token为uuid，则表明是正式用户，需要获取 token 对应的 loginId
                loginId = (String) StpUtil.getLoginIdByToken(tokenValue);
            }

            // 根据 loginId 获取用户，loginId 可以带有前缀（例如 super:1001 或者 number:1001）
            UserDetails userDetails = securityService.loadUserDetails(loginId);


            // --------------- 第二步： 校验 登录 ---------------

            // 检查控制器方法或其所属的控制器类是否有 @SaIgnore 注解
            if (SaStrategy.instance.isAnnotationPresent.apply(handlerMethod.getMethod(), SaIgnore.class)) {
                return true;
            }

            // 执行到这里，后面的代码都需要用于登录才能操作，所以要求 userDetails 一定是存在的
            // 读不到用户，可能是token错误或者userId不正确
            if (userDetails == null) {
                ResponseUtils.printError(response, ResultCode.LOGIN_STATE_INVALID);
                return false;
            }

            // 检测和更新 token 活跃频率
            checkActiveTimeout(userDetails, isDebugNumberToken);


            // --------------- 第三步： 校验 权限 ---------------

            SecurityContext.setCurrentUser(userDetails);

            // 超级管理员，不需要校验权限
            if (userDetails.getAdmin()) {
                return true;
            }

            // 检查控制器方法或其所属的控制器类是否有 @SaIgnore 注解
            if (SaStrategy.instance.isAnnotationPresent.apply(handlerMethod.getMethod(), SaIgnore.class)) {
                return true;
            }

            SaStrategy.instance.checkMethodAnnotation.accept(handlerMethod.getMethod());

        } catch (SaTokenException e) {
            /*
             * sa-token 异常状态码
             * 具体请看： https://sa-token.cc/doc.html#/fun/exception-code
             */
            int code = e.getCode();
            if (code == 11041 || code == 11051) {
                ResponseUtils.printError(response, ResultCode.NO_PERMISSION);
            } else if (code >= 11011 && code <= 11015) {
                ResponseUtils.printError(response, ResultCode.LOGIN_STATE_INVALID);
            } else if (code == 11016) {
                ResponseUtils.printError(response, ResultCode.LOGIN_ACTIVE_TIMEOUT);
            } else {
                ResponseUtils.printError(response, e.getMessage());
            }
            return false;
        } catch (Exception e) {
            ResponseUtils.printError(response, e.getMessage());
            log.error(e.getMessage(), e);
            return false;
        }

        // 通过验证
        return true;
    }

    /**
     * 检测 token 最低活跃频率（单位：秒），如果 token 超过此时间没有访问系统就会被冻结
     */
    private void checkActiveTimeout(UserDetails userDetails, boolean isDebugNumberToken) {

        // 对于开发环境的数字token ，不需要检测活跃有效期
        if (isDebugNumberToken) {
            return;
        }

        // 用户不在线，也不用检测
        if (userDetails == null) {
            return;
        }

        // 小于1 ，也不需要检测
        if (tokenActiveTimeout < 1) {
            return;
        }

        StpUtil.checkActiveTimeout();
        StpUtil.updateLastActiveToNow();
    }
}
